Understanding Concurrency, Threading, and Multiprocessing Fundamentals in Python
Course: Python Training
Topic: Understanding Concurrency, Threading, and Multiprocessing Fundamentals
Course Link: Python Training Course at JustAcademy
Demo Registration: Register for a Free Course Demo
Website: Visit JustAcademy
1. Introduction to Concurrency in Python
Concurrency is a programming technique that allows multiple tasks to make progress during overlapping periods. Instead of waiting for one operation to finish before starting another, a concurrent program can organize work so that tasks progress efficiently.
For example, a Python application might download several files, process customer records, respond to web requests, or generate reports. Concurrency can help the application handle these activities efficiently, depending on the type of work and available resources.
Python provides several approaches to concurrent execution, including the threading, multiprocessing, and concurrent.futures modules. Choosing the right approach depends on whether tasks spend most of their time waiting for input and output or performing CPU-intensive calculations.
Learning Objectives
- Understand concurrency, parallelism, and asynchronous execution.
- Identify CPU-bound and I/O-bound tasks.
- Create and manage threads using Python's
threading module.
- Execute tasks in separate processes using
multiprocessing.
- Understand the Global Interpreter Lock (GIL) in traditional CPython builds.
- Use locks, queues, and synchronization mechanisms safely.
- Apply thread pools and process pools with
concurrent.futures.
- Recognize race conditions, deadlocks, and other concurrency problems.
2. Understanding Concurrency and Parallelism
2.1 What Is Concurrency?
Concurrency means structuring a program so that multiple tasks can make progress over overlapping periods. The operating system or runtime may switch between tasks, allowing one task to proceed while another is waiting.
Example: A program downloads a file while another task processes an already downloaded file. The tasks can make progress without requiring the entire application to wait for every operation to finish sequentially.
2.2 What Is Parallelism?
Parallelism means executing multiple operations at the same instant, commonly on different CPU cores. Multiprocessing can provide parallel execution by running work in separate processes.
2.3 Concurrency vs. Parallelism
| Feature |
Concurrency |
Parallelism |
| Meaning |
Multiple tasks make progress over overlapping periods. |
Multiple tasks execute at the same time. |
| Primary objective |
Manage multiple tasks efficiently. |
Perform operations simultaneously. |
| Hardware requirement |
Can work on a single CPU core. |
Usually requires multiple execution resources. |
| Typical Python tools |
Threading, asyncio, thread pools. |
Multiprocessing and suitable process pools. |
| Example |
Handling multiple network requests while waiting for responses. |
Calculating separate portions of a large dataset on different CPU cores. |
A program can be concurrent without running tasks in parallel. It can also combine concurrency and parallelism when its design and environment support both.
3. Types of Workloads
3.1 CPU-Bound Tasks
A CPU-bound task spends most of its time performing calculations. Examples include mathematical simulations, image transformations, large data computations, and certain compression or encoding operations.
In traditional GIL-enabled CPython, multiple Python threads generally do not execute Python bytecode in parallel across CPU cores. For CPU-heavy Python code, multiprocessing may provide better parallelism, although the result depends on the workload and process overhead.
3.2 I/O-Bound Tasks
An I/O-bound task spends much of its time waiting for an external operation, such as a network response, file access, database query, or API request.
Threads are often useful for I/O-bound workloads because one thread can make progress while another waits for I/O. For large numbers of network operations, asynchronous programming with asyncio can also be appropriate.
3.3 Example of Sequential I/O
import time
def download_file(file_number):
print(f"Starting file {file_number}")
time.sleep(2) # Simulates waiting for network I/O
print(f"Finished file {file_number}")
start = time.perf_counter()
for number in range(1, 4):
download_file(number)
print(f"Total time: {time.perf_counter() - start:.2f} seconds")
Each simulated download waits for two seconds before the next one starts. The total runtime will be approximately six seconds, excluding small overheads.
4. Understanding Threads
4.1 What Is a Thread?
A thread is an execution path inside a process. A process can contain multiple threads that share the same process memory and resources, while each thread has its own execution state and stack.
Because threads share memory, they can communicate easily through shared objects. However, simultaneous access to mutable shared data can create race conditions if the operations are not coordinated.
4.2 Benefits of Threading
- Useful for I/O-bound work such as network requests and file operations.
- Threads can share data without repeatedly serializing and transferring it between processes.
- Thread pools make it easier to manage a collection of similar tasks.
- They can improve responsiveness when a program must handle several waiting operations.
4.3 Limitations of Threading
- Shared mutable data requires careful synchronization.
- Race conditions and deadlocks can be difficult to diagnose.
- Creating too many threads can consume memory and increase scheduling overhead.
- Traditional GIL-enabled CPython limits parallel execution of Python bytecode by multiple threads.
5. Creating Threads with the threading Module
Python's built-in threading module provides the Thread class and synchronization tools for creating and managing threads.
5.1 Basic Thread Example
import threading
import time
def task():
print(f"Task running in {threading.current_thread().name}")
time.sleep(2)
print("Task completed")
thread = threading.Thread(target=task, name="Worker-1")
thread.start()
thread.join()
print("Main program completed")
Explanation:
threading.Thread() creates a thread object.
target=task identifies the function that the thread will execute.
start() starts the thread; it does not directly call the target function in the main thread.
join() waits until the thread finishes.
current_thread() identifies the thread executing the code.
5.2 Starting Multiple Threads
import threading
import time
def process_item(item):
print(f"Processing item {item}")
time.sleep(1)
print(f"Completed item {item}")
threads = []
for item in range(1, 4):
thread = threading.Thread(target=process_item, args=(item,))
threads.append(thread)
thread.start()
for thread in threads:
thread.join()
print("All items processed")
The tasks overlap while waiting. Their output order may vary because the operating system controls thread scheduling. The comma in args=(item,) is important because it creates a one-element tuple.
5.3 Passing Arguments to Threads
import threading
def greet(name, message):
print(f"{message}, {name}!")
thread = threading.Thread(
target=greet,
args=("Aarav", "Welcome")
)
thread.start()
thread.join()
Use args for positional arguments and kwargs for keyword arguments. Pass the function itself as target, rather than calling it when constructing the thread.
6. Thread Lifecycle and Management
A thread commonly progresses through these stages:
- Created: The thread object exists but has not started.
- Started: The program calls
start().
- Running or waiting: The thread executes or waits for CPU time, I/O, or synchronization.
- Finished: The target function returns or terminates with an exception.
6.1 Useful Thread Methods
| Method or Property |
Purpose |
start() |
Starts a thread. |
run() |
Contains the target execution logic; calling it directly does not start a new thread. |
join() |
Waits for a thread to finish. |
is_alive() |
Checks whether the thread is still running. |
name |
Gets or sets the thread's descriptive name. |
daemon |
Controls whether a thread is a daemon thread; it should be set before starting. |
6.2 Daemon and Non-Daemon Threads
A non-daemon thread normally keeps the Python process alive until it finishes. Daemon threads do not prevent interpreter shutdown, so they may be stopped abruptly when the program exits.
Use non-daemon threads for work that must finish reliably, such as saving important results. Daemon threads can be suitable for certain background tasks that do not require guaranteed completion, but graceful shutdown is generally preferable.
7. Thread Synchronization and Race Conditions
7.1 What Is a Race Condition?
A race condition occurs when a result depends on the timing or interleaving of concurrent operations. For example, two threads may read the same counter value and both write back an incremented value, causing an update to be lost.
7.2 Demonstrating Shared Mutable State
import threading
counter = 0
lock = threading.Lock()
def increment():
global counter
for _ in range(100_000):
with lock:
counter += 1
threads = [
threading.Thread(target=increment)
for _ in range(4)
]
for thread in threads:
thread.start()
for thread in threads:
thread.join()
print("Final counter:", counter)
# Expected: 400000
The lock ensures that each increment operation is performed within a protected critical section. Do not assume that a multi-step operation on shared data is automatically safe just because individual Python operations may be protected internally.
7.3 Using Lock Objects
A Lock allows only one thread at a time to enter a protected section. A context manager releases the lock even if an exception occurs inside the block.
import threading
balance = 1000
balance_lock = threading.Lock()
def deposit(amount):
global balance
with balance_lock:
balance += amount
deposit(500)
print(balance) # 1500
7.4 Other Synchronization Tools
| Tool |
Purpose |
Typical Use |
Lock |
Protects a critical section. |
Updating a shared counter. |
RLock |
Allows the owning thread to acquire the same lock repeatedly. |
Nested methods that use the same lock. |
Semaphore |
Limits the number of concurrent users of a resource. |
Restricting simultaneous connections. |
Event |
Signals that a condition or event has occurred. |
Requesting worker shutdown. |
Condition |
Coordinates threads that wait for a condition to become true. |
Producer-consumer coordination. |
Barrier |
Makes a group of threads wait until all reach a point. |
Coordinating stages of parallel work. |
8. Thread Communication Using Queue
The queue module provides thread-safe queues for passing work or results between threads. A queue is often safer and easier to maintain than having multiple threads manipulate the same list or dictionary without coordination.
import threading
from queue import Queue
tasks = Queue()
def worker():
while True:
item = tasks.get()
try:
if item is None:
return
print("Processing:", item)
finally:
tasks.task_done()
thread = threading.Thread(target=worker)
thread.start()
for item in ["report.csv", "image.png", "data.json"]:
tasks.put(item)
tasks.put(None)
tasks.join()
thread.join()
print("Queue processing finished")
In this example, task_done() is called for each item retrieved, including the shutdown marker. The worker returns after receiving the marker, and join() waits for the queue's unfinished task count to reach zero.
9. Understanding Multiprocessing
9.1 What Is Multiprocessing?
Multiprocessing executes tasks in separate operating-system processes. Each process generally has its own Python interpreter and memory space. On multi-core systems, independent processes can run Python calculations in parallel.
The multiprocessing module supports process creation, pools, inter-process communication, and synchronization. Because processes have separate memory, sharing information usually requires queues, pipes, shared memory, or other explicit communication mechanisms.
9.2 Benefits of Multiprocessing
- Can run CPU-bound Python work in parallel across multiple cores.
- Separate process memory can reduce some forms of accidental shared-state interference.
- A failure in one worker process is often more isolated from the others than a thread failure.
- Useful for computationally expensive tasks that can be divided into independent units.
9.3 Limitations of Multiprocessing
- Starting processes usually costs more time and memory than starting threads.
- Arguments and results often need to be serialized and transferred between processes.
- Shared data and synchronization require additional design.
- Functions and data sent to process workers must meet serialization and platform requirements.
10. Creating Processes with multiprocessing
10.1 Basic Process Example
import multiprocessing
import os
def worker(number):
print(
f"Task {number} running in process {os.getpid()}"
)
if __name__ == "__main__":
processes = []
for number in range(1, 4):
process = multiprocessing.Process(
target=worker,
args=(number,)
)
processes.append(process)
process.start()
for process in processes:
process.join()
print("All processes completed")
Explanation:
Process() creates a process object.
start() starts execution in a separate process.
join() waits for the process to finish.
os.getpid() returns the current process ID.
- The
if __name__ == "__main__": guard prevents child processes from repeatedly executing the program's process-creation code under spawn-based startup.
Important: Keep process creation inside the main guard, especially on Windows and other environments that use the spawn start method. Test multiprocessing code in a regular Python script because some interactive environments cannot import worker functions correctly.
10.2 Process Start Methods
Python supports different process start methods, including spawn, fork, and, on supported platforms and versions, forkserver. Availability and defaults vary by operating system and Python version.
- spawn: Starts a fresh Python interpreter and initializes a new process.
- fork: Creates a child by forking the parent process on supported systems. Forking a process with active threads can be unsafe in some situations.
- forkserver: Uses a separate server process to create children on supported systems.
Do not rely on one start method being the default everywhere. Use multiprocessing.get_context() when a specific context is needed, and check the documentation for the Python version and platform being used.
11. Using Process Pools
A process pool maintains a group of worker processes and distributes tasks among them. This avoids manually creating a new process for every small calculation.
from multiprocessing import Pool
def square(number):
return number * number
if __name__ == "__main__":
numbers = [1, 2, 3, 4, 5]
with Pool(processes=3) as pool:
results = pool.map(square, numbers)
print(results)
# [1, 4, 9, 16, 25]
The map() method applies a function to each item and returns results in input order. Pool workers are useful when many independent CPU-bound tasks can be distributed efficiently.
12. Comparing Threading and Multiprocessing
| Feature |
Threading |
Multiprocessing |
| Execution unit |
Thread inside a process. |
Separate operating-system process. |
| Memory |
Threads share process memory. |
Processes generally have separate memory spaces. |
| Communication |
Shared objects, queues, events, and locks. |
Queues, pipes, shared memory, managers, and serialized messages. |
| Startup overhead |
Usually lower. |
Usually higher. |
| I/O-bound work |
Often a good choice. |
Useful in some cases, but can add unnecessary overhead. |
| CPU-bound Python code |
Often limited by the GIL in traditional CPython builds. |
Can use multiple CPU cores in parallel. |
| Synchronization |
Protect shared mutable state. |
Coordinate access to shared resources and inter-process communication. |
| Common API |
threading.Thread |
multiprocessing.Process |
13. Using concurrent.futures
The concurrent.futures module provides a high-level interface for running tasks asynchronously with thread pools or process pools. It reduces the amount of manual worker-management code needed for many common workloads.
13.1 ThreadPoolExecutor
ThreadPoolExecutor manages a pool of threads. It is often convenient for I/O-bound tasks such as requesting multiple web pages, processing files, or communicating with remote services.
from concurrent.futures import ThreadPoolExecutor
import time
def fetch_record(record_id):
time.sleep(1) # Simulates I/O waiting
return f"Record {record_id} fetched"
if __name__ == "__main__":
record_ids = [101, 102, 103, 104]
with ThreadPoolExecutor(max_workers=4) as executor:
results = list(executor.map(fetch_record, record_ids))
for result in results:
print(result)
The executor manages the worker threads. Using it in a with statement ensures that the executor is shut down cleanly when the block ends.
13.2 ProcessPoolExecutor
ProcessPoolExecutor provides a similar interface but runs tasks in separate processes, making it a good option to evaluate for CPU-intensive workloads.
from concurrent.futures import ProcessPoolExecutor
def calculate_square(number):
return number * number
if __name__ == "__main__":
numbers = [10, 20, 30, 40, 50]
with ProcessPoolExecutor(max_workers=3) as executor:
results = list(executor.map(calculate_square, numbers))
print(results)
# [100, 400, 900, 1600, 2500]
Worker functions should generally be defined at the module level and arguments and return values should be serializable. The main guard is important for portable multiprocessing code.
13.3 The submit() Method and Future Objects
submit() schedules a function call and returns a Future object. A future represents the eventual result or exception of a submitted task.
from concurrent.futures import ThreadPoolExecutor
def multiply(a, b):
return a * b
with ThreadPoolExecutor(max_workers=2) as executor:
future = executor.submit(multiply, 6, 7)
result = future.result()
print(result) # 42
The result() method returns the completed result and waits if necessary. If the task raises an exception, retrieving the result will raise that exception in the calling code.
14. The Global Interpreter Lock (GIL)
The Global Interpreter Lock, or GIL, is a mechanism in traditional GIL-enabled CPython builds that permits only one thread at a time to execute Python bytecode within a given interpreter. This can limit CPU-bound parallelism with ordinary threads.
The GIL does not mean threads are useless. Threads remain useful for I/O-bound tasks because the runtime can switch between threads while they wait. Some native libraries also release the GIL while performing computations.
Python also supports free-threaded CPython builds that can run without the traditional GIL when configured appropriately. Therefore, GIL-related performance behavior depends on the Python implementation, build, libraries, and workload.
Key Points About the GIL
- It is primarily relevant to CPython; other Python implementations may behave differently.
- It can limit parallel execution of Python bytecode in traditional CPython builds.
- It does not eliminate the need for locks or correct synchronization.
- Multiprocessing can bypass a single interpreter's GIL limitation by using separate processes.
- Benchmark the actual workload instead of assuming one approach is always faster.
15. Deadlocks and Common Concurrency Problems
15.1 What Is a Deadlock?
A deadlock occurs when two or more tasks wait indefinitely for resources or events that cannot become available because of the way the tasks are coordinated.
For example, Thread A may hold Lock 1 and wait for Lock 2, while Thread B holds Lock 2 and waits for Lock 1. Neither thread can proceed.
15.2 Preventing Deadlocks
- Acquire multiple locks in a consistent order.
- Keep critical sections short.
- Avoid waiting for another task while holding a lock that it may need.
- Use timeouts where appropriate and handle timeout results correctly.
- Design task dependencies carefully when using futures and executors.
15.3 Common Mistakes
| Mistake |
Why It Is a Problem |
Recommended Practice |
| Calling a thread target directly |
The function runs in the current thread. |
Use start() to start a new thread. |
| Forgetting to join important workers |
The main code may proceed before expected work finishes. |
Join threads or use managed executors. |
| Sharing mutable data without coordination |
Updates may conflict or results may be inconsistent. |
Use locks, queues, or a design that avoids shared mutation. |
| Creating too many workers |
Scheduling and memory overhead may reduce performance. |
Measure and select an appropriate worker count. |
| Omitting the process main guard |
Spawn-based systems may repeat process-creation code. |
Protect process startup with if __name__ == "__main__":. |
| Assuming task completion order |
Concurrent tasks can finish in different orders. |
Use ordered result APIs or explicitly coordinate ordering. |
16. Error Handling and Graceful Shutdown
Concurrent programs should handle exceptions, wait for required work to finish, and shut down workers in a predictable way. A thread's exception does not automatically behave like an exception raised by a direct function call in the main thread.
With an executor, exceptions are commonly observed when retrieving a future's result:
from concurrent.futures import ThreadPoolExecutor
def divide(a, b):
return a / b
with ThreadPoolExecutor(max_workers=2) as executor:
future = executor.submit(divide, 10, 0)
try:
print(future.result())
except ZeroDivisionError as error:
print("Task failed:", error)
For long-running workers, design a clear stop signal, avoid abrupt termination where possible, and make sure resources such as files, sockets, and database connections are closed safely.
17. Measuring Concurrent Performance
Concurrency does not automatically make a program faster. Process startup, task scheduling, communication, locks, and data transfer can add overhead. Small tasks may finish faster when executed sequentially.
Use time.perf_counter() to measure elapsed time:
import time
start = time.perf_counter()
# Run the task or workload being measured here.
elapsed = time.perf_counter() - start
print(f"Elapsed time: {elapsed:.4f} seconds")
When comparing sequential and concurrent versions:
- Use the same input data and computational work.
- Measure several runs where practical.
- Account for process startup and data-transfer costs.
- Check that both implementations produce the same results.
- Measure memory usage as well as elapsed time when relevant.
18. Practical Mini-Project: Process Several Tasks Concurrently
Project objective: Simulate processing multiple customer records using a thread pool. This example demonstrates how independent I/O-bound tasks can overlap.
Project Code
from concurrent.futures import ThreadPoolExecutor
import time
def process_customer(customer_id):
print(f"Processing customer {customer_id}")
time.sleep(1) # Simulates an I/O operation
return {
"customer_id": customer_id,
"status": "processed"
}
def main():
customer_ids = [1001, 1002, 1003, 1004, 1005]
start = time.perf_counter()
with ThreadPoolExecutor(max_workers=3) as executor:
results = list(
executor.map(process_customer, customer_ids)
)
elapsed = time.perf_counter() - start
print("\nProcessing results:")
for result in results:
print(result)
print(f"\nTotal elapsed time: {elapsed:.2f} seconds")
if __name__ == "__main__":
main()
Project Explanation
- The
process_customer() function simulates an I/O-bound operation.
- The executor uses up to three worker threads.
executor.map() schedules the work and returns results in the same order as the input IDs.
- The elapsed time measurement helps compare the concurrent version with a sequential implementation.
- The example does not access a real database or modify actual customer records.
Project Extensions
- Read customer IDs from a CSV file.
- Write successful results to a JSON file.
- Add exception handling for individual tasks.
- Compare sequential execution with a thread pool.
- Replace the simulated I/O operation with a safe test API or local file operation.
- Build a separate CPU-bound example using
ProcessPoolExecutor.
19. Best Practices for Concurrent Python Programs
- Choose the concurrency model based on the workload rather than habit.
- Prefer threads or asynchronous I/O for many I/O-bound tasks and consider processes for CPU-heavy Python calculations.
- Use
concurrent.futures when a thread or process pool provides a simpler solution than manual worker management.
- Limit worker counts to a reasonable number and benchmark performance.
- Use
queue.Queue to pass work safely between threads when appropriate.
- Protect shared mutable state with proper synchronization or redesign the program to avoid shared mutation.
- Use the main guard for multiprocessing code that needs to work with spawn-based process creation.
- Keep process-worker functions and their arguments compatible with the selected process execution environment.
- Handle task exceptions and define a graceful shutdown strategy.
- Do not rely on a particular order of concurrent output unless the program explicitly enforces it.
- Test concurrent code under realistic conditions, including failures and timeouts.
- Consult the documentation for the exact Python version and platform used in production.
20. Practice Exercises
- Create two threads that print different messages and wait for both to finish using
join().
- Write a thread function that accepts a list of numbers and returns a summary through a thread-safe queue.
- Use a lock to safely increment a shared counter from four threads.
- Write a multiprocessing program that calculates the squares of a list of integers.
- Use
ThreadPoolExecutor to simulate five network requests and measure elapsed time.
- Use
ProcessPoolExecutor to run a CPU-heavy calculation on several independent inputs.
- Modify a concurrent program to handle an exception raised by a worker task.
- Compare sequential execution, threading, and multiprocessing for a workload suitable for each approach.
- Explain how a deadlock can happen when two locks are acquired in inconsistent orders.
- Write a short note explaining concurrency, parallelism, CPU-bound tasks, and I/O-bound tasks.
21. Interview Questions and Answers
Q1. What is concurrency?
Answer: Concurrency is the organization of multiple tasks so that they can make progress during overlapping periods. Tasks may alternate on one CPU core or execute in parallel on multiple cores.
Q2. What is the difference between concurrency and parallelism?
Answer: Concurrency allows tasks to make progress over overlapping periods, whereas parallelism means multiple tasks execute at the same time.
Q3. What is a thread?
Answer: A thread is an execution path within a process. Threads in the same process share memory and resources.
Q4. What is a process?
Answer: A process is an operating-system execution environment with its own memory space and resources. Separate processes can execute CPU-bound work in parallel.
Q5. When should threading be used?
Answer: Threading is often appropriate for I/O-bound tasks, such as network communication, file operations, and waiting for external services.
Q6. When should multiprocessing be used?
Answer: Multiprocessing is often a good option for CPU-bound Python work that can be divided into independent tasks and can justify process startup and communication costs.
Q7. What is the GIL?
Answer: The Global Interpreter Lock is a mechanism in traditional GIL-enabled CPython builds that allows only one thread at a time to execute Python bytecode within an interpreter. Free-threaded builds and other Python implementations may behave differently.
Q8. What does join() do?
Answer: join() blocks the calling thread until the specified thread or process has finished, unless a timeout is supplied.
Q9. What is a race condition?
Answer: A race condition is a problem where a result depends on the timing or interleaving of concurrent operations. Locks and other coordination mechanisms can help prevent it.
Q10. What is a deadlock?
Answer: A deadlock occurs when tasks wait indefinitely for resources or events that cannot become available because of their dependencies.
Q11. What is a thread pool?
Answer: A thread pool manages a group of reusable worker threads to execute submitted tasks without manually creating a new thread for each task.
Q12. What is ProcessPoolExecutor?
Answer: It is a high-level API from concurrent.futures that schedules function calls in a pool of worker processes.
Q13. Why is the main guard important in multiprocessing?
Answer: It prevents process-creation code from being unintentionally rerun when a child process imports the main module under spawn-based startup.
Q14. Why use a queue for communication between threads?
Answer: A thread-safe queue allows workers to exchange work items or results with built-in coordination, reducing the need for manual locking around a shared collection.
Q15. Does concurrency always improve performance?
Answer: No. Scheduling, synchronization, process creation, and communication can introduce overhead. Performance depends on the workload and should be measured.
22. Quick Revision Checklist
- Concurrency and parallelism are related but different concepts.
- CPU-bound tasks mainly use processing time; I/O-bound tasks mainly wait for external operations.
threading.Thread creates and manages threads.
multiprocessing.Process creates separate processes.
start() starts execution; join() waits for completion.
- Locks and queues help coordinate access to shared data.
- Race conditions and deadlocks are common concurrency risks.
ThreadPoolExecutor and ProcessPoolExecutor simplify worker management.
- The GIL's impact depends on the Python implementation and build.
- Multiprocessing code should use a suitable main guard and compatible worker functions.
- Benchmark real workloads before choosing an approach.
23. Conclusion
Concurrency is an important Python programming concept for building responsive applications and managing multiple tasks efficiently. Threading is often useful when tasks spend substantial time waiting for I/O, while multiprocessing can help CPU-intensive work take advantage of multiple cores.
Understanding thread lifecycle, process management, synchronization, race conditions, deadlocks, and executor pools makes it easier to build reliable concurrent applications. Start with small examples, measure performance, and introduce complexity only when the workload requires it.
To continue learning Python programming, explore the Python Training Course at JustAcademy, register for a course demo, or visit the JustAcademy website.
Additional Learning Resource
For further technical reference, read the official Python documentation on concurrent execution.